Repository navigation
feat(runtime): rooted wrapper lifetime mode - #559
Draft
edusperoni wants to merge 2 commits into
Draft
edusperoni wants to merge 2 commits into
edusperoni wants to merge 2 commits into
Conversation
Adds the app config "wrapperLifetime": { "mode", "scope" }, read once
per process. "resurrecting" (default) is the existing behaviour. "rooted"
stops relying on resurrecting finalizers for ObjC object wrappers:
- An adopted wrapper's handle is a phantom (kParameter) weak handle
instead of a kFinalizer one. Its first pass only queues the
registration; the native object is released later, on the runtime's
thread, from the event loop or on entry to a native call.
- A wrapper in scope is a strong root while something other than the
runtime retains its object. A major-GC prologue compares -retainCount
(the first non-JS implementation) against the runtime's own retains,
counted process-wide, and promotes or demotes. Wrappers are born strong
and made strong again when they cross the JS/native boundary.
- "scope": "extended" (default) roots only JS-extended instances; "all"
roots every ObjC wrapper except blocks and tagged pointers.
- The extended classes' retain/release swizzle is not installed in
"rooted".
A JS-unreachable holder that native code owns therefore stays rooted
with everything it references, so nothing it reaches is released under
it (the ListView shape in WrapperLifetimeModesTests.js).
Cache readers drop an Instances entry whose handle is empty (its release
is still queued) instead of handing out a dead wrapper. Teardown releases
whatever the queue holds through Caches::ExistingStateFor, since the
isolate is already invalidated by then.
ns:runtime gains the read-only config keys wrapperLifetimeMode and
wrapperLifetimeScope, and the promotions, demotions and queuedReleases
counters.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second of the stacked wrapper-lifetime series, on top of #558. Adds a config flag for how ObjC object wrappers live, and a
rootedmode that does not rely on resurrecting finalizers. The default staysresurrecting, so nothing changes unless an app opts in.Current behavior
kFinalizerweak callback. When the JS object becomes unreachable, the finalizer releases the native object, unless the object is a JS-extended instance that native code still retains. In that case the finalizer refuses and the wrapper is resurrected.New behavior
Config. The app config takes
"wrapperLifetime": { "mode": "resurrecting" | "rooted", "scope": "extended" | "all" }. It is read once per process; unknown values log a warning and fall back to the defaults (resurrecting,extended).ns:runtimeexposes both values as read-only config keys,wrapperLifetimeModeandwrapperLifetimeScope.rootedmode.kFinalizercallback. Its handle is a strong root while something other than the runtime retains its object, and a phantom (kParameter) weak handle otherwise.-retainCountagainst the retains the runtime itself holds, counted across isolates. It uses the first non-JS-retainCountimplementation, since JS cannot run in a GC prologue. Wrappers are born strong, and are made strong again whenever they cross the JS/native boundary: returned from native code, or passed as a receiver or argument.-deallocmay run."extended"roots only instances of JS-extended classes."all"roots every ObjC wrapper except blocks and tagged pointers.A JS-unreachable holder that native code owns therefore stays rooted together with everything it references, so nothing it reaches is released under it. This includes
WeakReftargets andWeakMapentries keyed on it.Developer contract under
rooted. It follows the ARC rules: a native object that holds JS which references it back is a cycle, and stays alive until one side lets go. Break such back references with aWeakRef. Later PRs in the series add a portablecanarymode alongside these two.Other changes.
FindCachedInstance,CreateJsWrapper, the method-callback self-wrap sites) drop anInstancesentry whose handle is empty, because its release is still queued, instead of handing out a dead wrapper.Caches::ExistingStateFor<T>reads a state slot afterInvalidateIsolatefor this.getWrapperLifetimeStats()gainspromotions,demotionsandqueuedReleases.Docs:
docs/ns-builtin-modules.md,docs/knowledge/v8-resurrecting-finalizers.md,types/ns-runtime.d.ts.Testing
WrapperLifetimeModesTests.js, mode-aware: natively owned extended objects keep their JS state and everything it references;WeakMap/WeakRefsurvive underall; JS-only objects collect; release after native code lets go;WeakRef-broken cycles collect; the ListView shape (aWeakRefnever hands back a view whose native objects were released); a JS-implementedretainCount; explicit release; counters.resurrecting/extended,rooted/allandrooted/extended. ASan,rooted/all: 0 failures, no reports.WrapperLifetimeModesTests.jsnames the exercised mode in a spec title, and each rooted run's log was checked for it (an earlier round had set the config under the wrong key and silently ran the default mode). CI runs the default mode; the rooted runs inject the config intoTestRunner/app/package.jsonlocally.Performance
The default
resurrectingmode only gains:Escapeon native calls, argument writes and cache hits;Instanceshits.The resurrecting-only
retain/releaseswizzle is installed as before. Release A/B against #558, simulator, 10 alternating runs per side (order reversed for the second five), all bench specs. Medians:NSObject.alloc().init()(wrapper churn)obj.isEqual(obj)UIScreen.mainScreen.bounds.size.width(full chain)NSData.dataWithData(Uint8Array 64 B)small.isEqualToData(fresh on-heap Uint8Array(16))Every other case is within ±3%. The largest moves go both ways, and one (
cached struct: bounds.size, 25 → 27 ns) is on a path this PR does not touch, so they read as layout and run noise rather than a cost.V8 dependency
rooteditself needs no V8 patch: it uses phantom callbacks and GC prologue callbacks only. Theresurrectingdefault still needsv8_resurrecting_finalizers.patch.